package com.friendfeed.api;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;

public class FriendFeedServiceImpl implements FriendFeedService {
    private String nickname;
    private String remoteKey;
    private String service;
    private int start;
    private int num;
    private String via;

    public FriendFeedServiceImpl() {
    }

    public FriendFeedServiceImpl(String nickname, String remoteKey) {
        this.nickname = nickname;
        this.remoteKey = remoteKey;
    }

    public void setService(String service) {
        this.service = service;
    }

    public void setStart(int start) {
        this.start = start;
    }

    public void setNum(int num) {
        this.num = num;
    }

    private HttpClient getHttpClient() {
        HttpClient client = new HttpClient();

        if (nickname != null) {
            client.getState().setCredentials(
                    new AuthScope("friendfeed.com", 80),
                    new UsernamePasswordCredentials(nickname, remoteKey)
            );
        }
        return client;
    }

    private JSONObject post(String url, Object... params) {
        System.out.println("Posting " + url);
        
        HttpClient client = getHttpClient();
        PostMethod post = new PostMethod(url);

        if (params.length % 2 != 0) {
            throw new FriendFeedServiceException("Parameters must be of an even number", POST_ARGS_ERROR);
        }

        for (int i = 0; i < params.length; i = i + 2) {
            if (!(params[i] instanceof String)) {
                throw new FriendFeedServiceException("Odd numbered parameters must be of type String", POST_ARGS_ERROR);
            }

            post.addParameter(params[i].toString(), params[i + 1].toString());
        }

        if (via != null) {
            post.addParameter("via", via);
        }

        return request(client, post);
    }

    private void checkNickname(String nickname) {
        if (nickname == null) {
            throw new NullPointerException("Nickname cannot be null");
        }
    }

    private String fixupQueryUrl(String url) {
        StringBuilder sb = new StringBuilder(url);
        boolean queryAlreadyIncluded = url.contains("?");

        queryAlreadyIncluded = appendParam(sb, queryAlreadyIncluded, "service", service);

        if (start > 0) {
            queryAlreadyIncluded = appendParam(sb, queryAlreadyIncluded, "start", start);
        }

        if (num > 0) {
            appendParam(sb, queryAlreadyIncluded, "num", num);
        }

        return sb.toString();
    }

    private boolean appendParam(StringBuilder sb, boolean queryAlreadyIncluded, String name, Object value) {
        if (value != null) {
            if (!queryAlreadyIncluded) {
                sb.append("?");
            }

            try {
                if (queryAlreadyIncluded) {
                    sb.append("&");
                }

                sb.append(name).append("=").append(URLEncoder.encode(value.toString(), "UTF-8"));
                queryAlreadyIncluded = true;
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException("UTF-8 not supported?!", e);
            }
        }

        return queryAlreadyIncluded;
    }

    private List<Entry> fetchEntries(String url) {
        try {
            JSONObject entryWrapper = fetch(url);
            JSONArray entriesArray = entryWrapper.getJSONArray("entries");
            ArrayList<Entry> entries = new ArrayList<Entry>(entriesArray.length());

            for (int i = 0; i < entriesArray.length(); i++) {
                JSONObject jsonObject = entriesArray.getJSONObject(i);
                Entry entry = new Entry(jsonObject);
                entries.add(entry);
            }

            return entries;
        } catch (JSONException e) {
            throw new FriendFeedServiceException("Error parsing JSON", JSON_PARSE_ERROR, e);
        } catch (ParseException e) {
            throw new FriendFeedServiceException("Error parsing date", DATE_PARSE_ERROR, e);
        }
    }

    private JSONObject fetch(String url) {
        System.out.println("Fetching " + url);
        HttpClient client = getHttpClient();

        GetMethod get = new GetMethod(url);

        return request(client, get);
    }

    private JSONObject request(HttpClient client, HttpMethod httpMethod) {
        if (nickname != null) {
            httpMethod.setDoAuthentication(true);
        }

        try {
            int responseCode = client.executeMethod(httpMethod);
            InputStream is = httpMethod.getResponseBodyAsStream();
            StringBuilder sb = new StringBuilder();
            FriendFeedUtils.writeInputStream(is, sb);

            if (responseCode == 200) {
                return new JSONObject(sb.toString());
            } else {
                String errorCode = ERROR;
                try {
                    JSONObject errorObj = new JSONObject(sb.toString());
                    errorCode = errorObj.getString("errorCode");
                } catch (Exception e) {
                    // this is fine, not critical
                }

                if (responseCode == 401) {
                    throw new AuthorizationRequiredException(errorCode);
                } else if (responseCode == 403) {
                    throw new ForbiddenException(errorCode);
                } else {
                    throw new UnexpectedResponseException(responseCode, errorCode);
                }
            }
        } catch (IOException e) {
            throw new FriendFeedServiceException("Error communicating with FriendFeed", IO_ERROR, e);
        } catch (JSONException e) {
            throw new FriendFeedServiceException("Error parsing JSON", JSON_PARSE_ERROR, e);
        }
    }

    public List<Entry> fetchPublicFeed() {
        return fetchEntries(fixupQueryUrl("http://friendfeed.com/api/feed/public"));
    }

    public List<Entry> fetchUserFeed(String nickname) {
        checkNickname(nickname);

        return fetchEntries(fixupQueryUrl("http://friendfeed.com/api/feed/user/" + nickname));
    }

    public List<Entry> fetchHomeFeed() {
        return fetchEntries(fixupQueryUrl("http://friendfeed.com/api/feed/home"));
    }

    public List<Entry> fetchMultiUserFeed(List<String> nicknames) {
        return fetchEntries(fixupQueryUrl("http://friendfeed.com/api/feed/user?nickname=" + FriendFeedUtils.join(nicknames, ",")));
    }

    public void hide(String id) {
        post("http://friendfeed.com/api/entry/hide", "entry", id);
    }

    public void unhide(String id) {
        post("http://friendfeed.com/api/entry/hide", "entry", id, "unhide", "true");
    }

    public List<Entry> fetchRoomFeed(String roomNickname) {
        return fetchEntries(fixupQueryUrl("http://friendfeed.com/api/feed/room/" + roomNickname));
    }

    public List<Entry> fetchUserComments() {
        checkNickname(nickname);

        return fetchEntries(fixupQueryUrl("http://friendfeed.com/api/feed/user/" + nickname + "/comments"));
    }

    public User fetchProfile(String nickname) {
        try {
            JSONObject jsonObject = fetch("http://friendfeed.com/api/user/" + nickname + "/profile");
            return new User(jsonObject);
        } catch (JSONException e) {
            throw new FriendFeedServiceException("Error parsing JSON", JSON_PARSE_ERROR, e);
        }
    }

    public void addLike(String entryId) {
        post("http://friendfeed.com/api/like", "entry", entryId);
    }

    public void deleteLike(String entryId) {
        post("http://friendfeed.com/api/like/delete", "entry", entryId);
    }

    public Entry publishLink(String comment, String link) {
        try {
            JSONObject jsonObject = post("http://friendfeed.com/api/share", "title", comment, "link", link);

            return new Entry(jsonObject.getJSONArray("entries").getJSONObject(0));
        } catch (JSONException e) {
            throw new FriendFeedServiceException("Error parsing JSON", JSON_PARSE_ERROR, e);
        } catch (ParseException e) {
            throw new FriendFeedServiceException("Error parsing date", DATE_PARSE_ERROR, e);
        }
    }

    public void addComment(String entryId, String body) {
        post("http://friendfeed.com/api/comment", "entry", entryId, "body", body);
    }

    public List<Entry> fetchEntryById(String... ids) {
        StringBuilder sb = new StringBuilder("http://friendfeed.com/api/feed/entry?entry_id=");
        for (String id : ids) {
            sb.append(id).append(",");
        }
        sb.deleteCharAt(sb.length() - 1);

        return fetchEntries(fixupQueryUrl(sb.toString()));
    }

    public void setVia(String via) {
        this.via = via;
    }
}
